Skip to content

fix(desktop): event-driven tray menu + real org name#3458

Merged
saddlepaddle merged 1 commit into
mainfrom
tray-polling-fix
Apr 14, 2026
Merged

fix(desktop): event-driven tray menu + real org name#3458
saddlepaddle merged 1 commit into
mainfrom
tray-polling-fix

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Apr 14, 2026

Summary

  • Remove the 5s polling interval in apps/desktop/src/main/lib/tray/index.ts. The tray menu is now driven by host-service-coordinator status-changed events, mouse-enter on the tray icon, and the initial initTray() call.
  • Delete the hostInfoCache Map. updateTrayMenu() is now a single async function that fetches host.info for every active org in parallel (2s AbortController timeout) and builds the menu directly from the results — no persistent state, no sync/async split.
  • Unwrap the superjson envelope (result.data.json) when reading host.info. The original polling code read result.data and silently discarded every response, which is why the tray was falling back to orgId.slice(0, 8) (the UUID prefix) for every running service.

Test plan

  • bun run lint:fix — clean
  • bun run typecheck (apps/desktop) — clean
  • bun test (apps/desktop) — 1660 pass, 0 fail
  • Launch the desktop app with ≥1 running host-service, open the tray menu, and confirm each entry shows the real organization name + version instead of Loading… / a UUID slice
  • Start/stop a host-service and confirm the tray menu updates reactively via status-changed without needing to hover

Closes #3454


Summary by cubic

Make the desktop tray menu event-driven and show real organization names and versions instead of UUIDs or “Loading…”. Removes 5s polling and updates the menu on status changes and when hovering over the tray icon.

  • Bug Fixes

    • Read host.info from result.data.json, so entries display the correct organization name and version.
  • Refactors

    • Replace polling with event-driven updates: status-changed and tray mouse-enter.
    • Remove hostInfoCache; updateTrayMenu is async and fetches per active org in parallel with a 2s timeout (via AbortController).
    • Ignore unreachable services, and only rebuild the menu when the tray exists.
    • Restart/Stop actions now await a menu refresh.

Written for commit a595c31. Summary will update on new commits.

Summary by CodeRabbit

  • Refactor
    • Tray menu updates now respond to user interactions and application status changes rather than continuous polling, with loading indicators when host information is unavailable.

Replace the 5s tray polling interval with event-driven refresh:
`mouse-enter` + host-service status-changed trigger an async rebuild.
Drop the hostInfoCache Map — updateTrayMenu is now async and fetches
host.info inline with a 2s AbortController timeout, so there is no
stale state to manage. Unwrap the superjson envelope (`result.data.json`)
that the original polling code missed, which was causing every org to
render as a UUID slice (previously) or "Loading…" (after the first
pass of this fix).

Closes #3454
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The tray menu refresh mechanism was refactored from periodic 5-second polling to event-driven, on-demand updates. Host information is now fetched asynchronously with a 2-second timeout per active organization during menu updates, triggered by initialization, status changes, and mouse-enter events.

Changes

Cohort / File(s) Summary
Tray Menu Refresh Refactoring
apps/desktop/src/main/lib/tray/index.ts
Replaced periodic polling interval with event-driven updates. Removed global hostInfoCache and refreshHostInfo(). Added fetchHostInfo() async function with AbortController timeout for on-demand host info retrieval. Converted updateTrayMenu() to async and updated buildHostServiceSubmenu() to accept organization IDs and precomputed HostInfo map with fallback "Loading…" display.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 No more ticking clocks, no more endless wait,
Events now whisper when the tray should update,
Host info fetches swift with timeout's grace,
On-demand refreshes keep a lighter pace,
The menu blooms alive when you draw near! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The linked issue #3454 concerns keyboard shortcuts not matching the current layout (AZERTY), but the PR changes only affect tray menu polling and organization name display, which are unrelated to keyboard layout handling. The code changes do not address the keyboard layout issue. Verify that #3454 is the correct linked issue or update the implementation to fix keyboard shortcut mapping for different layouts.
Out of Scope Changes check ⚠️ Warning The changes are focused on tray menu event-driven updates and organization name display, which appear unrelated to the linked issue about keyboard shortcuts and layout handling. The PR scope (tray menu refactoring) does not align with the linked issue scope (keyboard layout shortcuts). Clarify the relationship between this PR and #3454 or verify the correct issue is linked.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(desktop): event-driven tray menu + real org name' clearly summarizes the main changes: replacing polling with event-driven updates and fixing organization name display.
Description check ✅ Passed The PR description is comprehensive, including a clear summary of changes, test plan with automated and manual testing steps, and the closing issue reference.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tray-polling-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 14, 2026

Greptile Summary

This PR fixes a long-standing bug where the desktop tray menu always displayed a UUID prefix instead of the real organization name. The root cause was that the original code read result.data from the tRPC response instead of result.data.json (the superjson envelope), silently discarding every successful response and falling back to orgId.slice(0, 8).

Along with the fix, the polling architecture is replaced with an event-driven one:

  • Root-cause fix: fetchHostInfo now reads data?.result?.data?.json, correctly unwrapping the superjson payload.
  • Polling removed: The 5 s setInterval and pollIntervalId are gone; tray updates are now triggered by host-service-coordinator status-changed events, mouse-enter on the tray icon, and the initial initTray() call.
  • Cache removed: hostInfoCache is deleted; updateTrayMenu is now a single async function that fetches all active orgs in parallel via Promise.all before building the menu.
  • Cleaner submenu builder: buildHostServiceSubmenu receives orgIds and a pre-fetched Map<string, HostInfo> instead of reading the cache internally.
  • Fallback label: UUID prefix fallback changed to \"Loading…\", appropriate for the "starting" state where the HTTP endpoint is not yet ready.

Confidence Score: 4/5

Safe to merge with one targeted fix: add a single-flight guard to prevent concurrent updateTrayMenu calls from racing to overwrite the tray menu with stale data.

The core bug fix (superjson envelope unwrapping) is correct and well-reasoned. The architectural shift from polling to event-driven updates is cleaner and eliminates stale-cache issues. The 2 s AbortController pattern is implemented correctly with finally-based cleanup. The only concrete concern is the absence of an in-flight guard: mouse-enter and status-changed can fire close together, launching two concurrent async update cycles where the later-finishing (possibly staler) call overwrites the menu. This is a real race but self-corrects on the next event, so it doesn't break the primary user path.

apps/desktop/src/main/lib/tray/index.ts — specifically the updateTrayMenu function and its two call sites in initTray.

Important Files Changed

Filename Overview
apps/desktop/src/main/lib/tray/index.ts Replaces polling + stale cache with event-driven async fetch; correctly unwraps superjson envelope (result.data.json). One P1 concern: no in-flight guard means concurrent updateTrayMenu calls can race and overwrite the menu with stale data.

Sequence Diagram

sequenceDiagram
    participant E as Electron Events
    participant T as Tray (index.ts)
    participant C as HostServiceCoordinator
    participant H as host-service HTTP

    Note over T: initTray()
    T->>T: void updateTrayMenu()
    T->>C: getActiveOrganizationIds()
    C-->>T: [orgId1, orgId2]
    par fetchHostInfo(orgId1)
        T->>H: GET /trpc/host.info (2s timeout)
        H-->>T: { result.data.json: { organization, version } }
    and fetchHostInfo(orgId2)
        T->>H: GET /trpc/host.info (2s timeout)
        H-->>T: { result.data.json: { ... } }
    end
    T->>T: buildHostServiceSubmenu(orgIds, infos)
    T->>T: tray.setContextMenu(menu)

    Note over E,T: status-changed event
    E->>T: status-changed
    T->>T: void updateTrayMenu() [same flow]

    Note over E,T: user hovers tray icon
    E->>T: mouse-enter
    T->>T: void updateTrayMenu() [same flow]
Loading

Reviews (1): Last reviewed commit: "fix(desktop): drive tray menu off events..." | Re-trigger Greptile

Comment on lines +185 to +199
async function updateTrayMenu(): Promise<void> {
if (!tray) return;

refreshHostInfo();

const coordinator = getHostServiceCoordinator();
const orgIds = coordinator.getActiveOrganizationIds();

const infoEntries = await Promise.all(
orgIds.map(async (orgId) => [orgId, await fetchHostInfo(orgId)] as const),
);
const infos = new Map<string, HostInfo>();
for (const [orgId, info] of infoEntries) {
if (info) infos.set(orgId, info);
}

if (!tray) return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 No in-flight guard on concurrent updateTrayMenu calls

Both the status-changed listener and the tray.on("mouse-enter") handler call void updateTrayMenu() independently, and updateTrayMenu now awaits up to 2 seconds of parallel network fetches. There is no guard preventing multiple calls from being in-flight simultaneously.

A concrete race: status-changed fires and starts call #1 (awaiting 2 s). The user then hovers the tray 300 ms later, starting call #2. Call #2 finishes first and sets the menu correctly. Then call #1 finishes and overwrites the menu with data derived from the orgIds snapshot it took 300 ms earlier — potentially excluding an org that was just added, or including one that was just stopped.

A lightweight fix is a single-flight flag that drops duplicate calls while one is in-progress:

let updateInFlight = false;

async function updateTrayMenu(): Promise<void> {
  if (updateInFlight) return;
  updateInFlight = true;
  try {
    // ... existing body ...
  } finally {
    updateInFlight = false;
  }
}

Alternatively, schedule a debounced microtask so rapid-fire events coalesce into one fetch cycle.

Comment on lines +139 to +143
const status = coordinator.getProcessStatus(orgId);
const info = infos.get(orgId);
const isRunning = status === "running";
const label = info?.organizationName ?? "Loading…";
const versionSuffix = info?.version ? ` (v${info.version})` : "";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 status can be "stopped" while label shows "Loading…"

buildHostServiceSubmenu calls coordinator.getProcessStatus(orgId) after the async fetch boundary in updateTrayMenu. getActiveOrganizationIds() filters to status !== "stopped", but between the snapshot and the menu build the coordinator state can change. If a service stops in that window, getProcessStatus returns "stopped", isRunning is false, and both action items are disabled — but the org is still rendered with a "Loading…" label (because fetchHostInfo returned null for a non-running process). The menu then shows:

Loading…
  stopped
  Restart  (disabled)
  Stop     (disabled)

This isn't catastrophic — the next status-changed event corrects it — but it's misleading UX. Consider re-reading orgIds after the await to filter to still-active orgs, or at least fall back to orgId.slice(0, 8) when the org is no longer in a running state.

@saddlepaddle saddlepaddle merged commit be9e000 into main Apr 14, 2026
6 of 7 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request Apr 15, 2026
…et-sh#3458)

Replace the 5s tray polling interval with event-driven refresh:
`mouse-enter` + host-service status-changed trigger an async rebuild.
Drop the hostInfoCache Map — updateTrayMenu is now async and fetches
host.info inline with a 2s AbortController timeout, so there is no
stale state to manage. Unwrap the superjson envelope (`result.data.json`)
that the original polling code missed, which was causing every org to
render as a UUID slice (previously) or "Loading…" (after the first
pass of this fix).

Closes superset-sh#3454
MocA-Love added a commit to MocA-Love/superset that referenced this pull request Apr 15, 2026
MocA-Love added a commit to MocA-Love/superset that referenced this pull request Apr 15, 2026
-s ours merge to record that upstream commits a3e34bf through
de70163 (13 commits) are semantically already present on origin/main
via the PR1-6 cherry-pick series (PRs #176, #177, #178, #179, #180,
#182), plus fork-adaptation fixes layered on top.

This merge target is de70163 specifically (not upstream/main) so
newer upstream commits (9fff075 and later) remain visible in future
behind counts.

Upstream commits covered by this audit merge:
- a3e34bf  fix(desktop): restore cmd+click requirement for v1 terminal file links (superset-sh#3457)  [PR1/#176]
- 57557f8  fix(desktop): gate v2 workspace children on collection readiness (superset-sh#3464)       [PR1/#176]
- 4ee2e61  fix(desktop): use native clipboard for copy path in v2 sidebar (superset-sh#3462)         [PR1/#176]
- 87d6e93  feat(desktop): close settings with Escape key (superset-sh#3466)                          [PR1/#176]
- 9c7f5f4  chore(desktop): auto-restart host-service on bundle change in dev (superset-sh#3461)      [PR1/#176]
- 93140d9  fix(mcp): accept MCP resource URL as valid OAuth audience (superset-sh#3459)              [PR2/#177]
- be9e000  fix(desktop): drive tray menu off events, fetch real org name (superset-sh#3458)          [PR2/#177]
- c5f791e  feat(v2): unify workspace delete through host-service (superset-sh#3443)                  [PR3/#178]
- 2c24d93  feat(desktop): paginated branch picker with checkout + open actions (superset-sh#3397)    [PR4/#179]
- 2bf1049  feat(desktop/hotkeys): v1 directional pane focus + best-effort v1 override migrator (superset-sh#3460)  [PR5/#180]
- 1294a7d  feat(desktop/hotkeys): restore Cmd+Alt+Arrow for tab/workspace nav (superset-sh#3472)    [PR5/#180]
- de70163  feat(desktop): v2 review tab first pass — PR info, checks, comments (superset-sh#3463)    [PR6/#182]

Intentionally skipped (version bump, fork has independent versioning):
- 1e23353  chore(desktop): bump version to 1.5.5 (superset-sh#3473)

Fork-adaptation fixes layered on top of the cherry-picks:
- PR1: host-service-coordinator alias import fix, settings Escape
       selector narrowing (role-based + popper wrapper), Escape
       close uses replace navigation
- PR2: dual quit mode preservation (requestQuit "release"/"stop"),
       trayUpdateToken guard for stale async fetchHostInfo results
- PR4: ChangesHeader.normalizeBranchName regex rewrite (lint false
       positive), worktree add uses fullRef for remote-tracking
       refs, syncTimedOut reset on pendingId change, GIT_REFS.md
       barrel example fix
- PR5: migrate.ts re-sanitize of existing localStorage overrides
       (v2 marker bump intent), FOCUS_PANE_* enabled:isActive for
       KeepAliveWorkspaces, CATEGORY_ORDER merges Navigation (upstream)
       and Browser (fork)
- PR6: normalizeThreadsToComments flattens all thread.comments (not
       just first), CommentPane overrides <a> (openUrl) and <img>
       (SafeImage), zero-badge suppression, pr-null comments gate

Fork features verified intact (Explore agent audit of combined
36d4de4..35d95f3 range):
- BROWSER_RELOAD / BROWSER_HARD_RELOAD hotkeys
- dual quit mode menu in tray
- v1 terminal cold-restore + retry reconnect (out of range but
  unaffected)
- KeepAliveWorkspaces (FOCUS_PANE_* gated on isActive)
- useCommandPalette + addMemoTab in v2 workspace
- host-service-coordinator rename alias pattern
@Kitenite Kitenite deleted the tray-polling-fix branch May 6, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keyboard shortcuts don't match the current keyboard layout

1 participant